// --------------------------------------------------------------------------------------------------------------------
// <copyright file="AssemblyCache.cs" company="Open Trader">
//   Copyright (c) David Denis (david.denis@systemathics.com)
// </copyright>
// <summary>
//   |  Open Trader - The Open Source Systematic Trading Platform
//   |
//   |  This program is free software: you can redistribute it and/or modify
//   |  it under the terms of the GNU General Public License as published by
//   |  the Free Software Foundation, either version 2 of the License, or
//   |  (at your option) any later version.
//   |
//   |  This program is distributed in the hope that it will be useful,
//   |  but WITHOUT ANY WARRANTY; without even the implied warranty of
//   |  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//   |  GNU General Public License for more details.
//   |
//   |  You should have received a copy of the GNU General Public License
//   |  along with this program.  If not, see http://www.gnu.org/licenses
//   |
//   |  Up to date informations about Open Trader can be found at :
//   |    http://opentrader.org
//   |    http://opentrader.codeplex.com
//   |
//   |  For professional services, please visit us at :
//   |    http://www.systemathics.com
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace Org.OpenTrader.Framework.LiveObjects
{
    #region Usings

    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Reflection;

    using Org.OpenTrader.Framework.Enums;
    using Org.OpenTrader.Framework.LiveObjects.Interfaces;

    #endregion

    /// <summary>
    /// The assembly cache.
    /// </summary>
    public class AssemblyCache : IDisposable
    {
        #region Constants and Fields

        /// <summary>
        /// The assemblyCache.
        /// </summary>
        private readonly IDictionary<string, AssemblyResource> assemblyCache =
            new Dictionary<string, AssemblyResource>();

        #endregion

        #region Constructors and Destructors

        /// <summary>
        /// Initializes a new instance of the <see cref="AssemblyCache"/> class.
        /// </summary>
        /// <param name="workingDirectory">
        /// The working directory.
        /// </param>
        /// <param name="resources">
        /// The resources.
        /// </param>
        public AssemblyCache()
        {
            AppDomain.CurrentDomain.AssemblyLoad += this.OnCurrentDomainAssemblyLoad;
            AppDomain.CurrentDomain.AssemblyResolve += this.OnCurrentDomainAssemblyResolve;
            AppDomain.CurrentDomain.TypeResolve += this.OnCurrentDomainTypeResolve;
        }

        #endregion

        #region Properties

        /// <summary>
        /// Gets AssemblyNames.
        /// </summary>
        public string[] AssemblyNames
        {
            get
            {
                return this.assemblyCache.Keys.ToArray();
            }
        }

        /// <summary>
        /// Gets WorkingDirectory.
        /// </summary>
        public string WorkingDirectory { get; private set; }

        #endregion

        #region Indexers

        /// <summary>
        /// The this.
        /// </summary>
        /// <param name="assemblyName">
        /// The assembly name.
        /// </param>
        public AssemblyResource this[string assemblyName]
        {
            get
            {
                if (this.assemblyCache.ContainsKey(assemblyName))
                {
                    return this.assemblyCache[assemblyName];
                }

                return null;
            }
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// The initialize.
        /// </summary>
        /// <param name="containerType">
        /// The container type.
        /// </param>
        /// <param name="workingDirectory">
        /// The working directory.
        /// </param>
        /// <param name="resources">
        /// The resources.
        /// </param>
        public void Initialize(ELiveObjectContainerType containerType, string workingDirectory, IResource[] resources)
        {
            this.WorkingDirectory = workingDirectory;

            foreach (var r in resources)
            {
                var resourceName = r.Name.Replace(r.Extension, string.Empty);
                Assembly resident = null;

                // Check if this assembly is resident (in the memory of current appdomain)
                foreach (var a in AppDomain.CurrentDomain.GetAssemblies())
                {
                    var assemblyName = new AssemblyName(a.FullName).Name;
                    if (assemblyName == resourceName)
                    {
                        resident = a;
                        break;
                    }
                }

                if (resident != null)
                {
                    // A resident assembly won't be loaded from byte [], it's already in memory. Set the Assembly property directly
                    this.assemblyCache[resourceName] = new AssemblyResource.Resident();
                    this.assemblyCache[resourceName].Assembly = resident;
                    this.assemblyCache[resourceName].UseForIntrospection = true;
                }
                else
                {
                    // A non resident assembly is loaded from byte []. Assembly property will be set on loading phase.
                    if (!this.assemblyCache.ContainsKey(resourceName))
                    {
                        this.assemblyCache[resourceName] = new AssemblyResource.Database();
                    }

                    if (r.Extension.Equals(".dll"))
                    {
                        this.assemblyCache[resourceName].Code = r.Content;
                        this.assemblyCache[resourceName].UseForIntrospection = r.IsAssembly;
                    }
                    else if (r.Extension.Equals(".pdb"))
                    {
                        this.assemblyCache[resourceName].Pdb = r.Content;
                    }
                }
            }

            foreach (var assemblyToLoad in this.assemblyCache)
            {
                if (assemblyToLoad.Value is AssemblyResource.Database && assemblyToLoad.Value.UseForIntrospection)
                {
                    this.LoadAssembly(assemblyToLoad.Key);
                }
            }
        }

        #endregion

        #region Implemented Interfaces

        #region IDisposable

        /// <summary>
        /// The dispose.
        /// </summary>
        public void Dispose()
        {
            AppDomain.CurrentDomain.AssemblyLoad -= this.OnCurrentDomainAssemblyLoad;
            AppDomain.CurrentDomain.AssemblyResolve -= this.OnCurrentDomainAssemblyResolve;
            AppDomain.CurrentDomain.TypeResolve -= this.OnCurrentDomainTypeResolve;
        }

        #endregion

        #endregion

        #region Methods

        /// <summary>
        /// The load.
        /// </summary>
        /// <param name="aname">
        /// The aname.
        /// </param>
        /// <param name="code">
        /// The code.
        /// </param>
        /// <param name="symstore">
        /// The symstore.
        /// </param>
        /// <returns>
        /// </returns>
        private Assembly Load(AssemblyName aname, byte[] code, byte[] symstore)
        {
            Assembly a;
            try
            {
                if (symstore == null)
                {
                    // This can throw exception, especially for unsafe code load, this is normal situation see catch clause below
                    a = AppDomain.CurrentDomain.Load(code);
                }
                else
                {
                    // This can throw exception, especially for unsafe code load, this is normal situation see catch clause below
                    a = AppDomain.CurrentDomain.Load(code, symstore);
                }
            }
            catch (FileLoadException)
            {
                // Try to load from file into this location 
                a = Devel.NoThrow(() => Assembly.LoadFile(this.WorkingDirectory + "\\" + aname.Name + ".dll"));
            }

            return a;
        }

        /// <summary>
        /// The load assembly.
        /// </summary>
        /// <param name="vname">
        /// The vname.
        /// </param>
        /// <returns>
        /// </returns>
        private Assembly LoadAssembly(string vname)
        {
            var aname = new AssemblyName(vname);
            var name = aname.Name;

            // Devel.ExecutedFrom(string.Format("Looking for {0}" , name));
            if (this.assemblyCache.ContainsKey(name))
            {
                var ar = this.assemblyCache[name];
                if (ar.Assembly != null)
                {
                    // Devel.ExecutedFrom(string.Format(" From cache"));
                    return ar.Assembly;
                }

                ar.Assembly = this.Load(aname, ar.Code, ar.Pdb);
                return ar.Assembly;
            }

            return null;
        }

        /// <summary>
        /// The on current domain assembly load.
        /// </summary>
        /// <param name="sender">
        /// The sender.
        /// </param>
        /// <param name="args">
        /// The args.
        /// </param>
        private void OnCurrentDomainAssemblyLoad(object sender, AssemblyLoadEventArgs args)
        {
            // Assembly just loaded
            var aname = new AssemblyName(args.LoadedAssembly.FullName);

            // The byte [] resources has been consumed as assembly/symstore, release them to spare memory)
            if (this.assemblyCache.ContainsKey(aname.Name))
            {
                this.assemblyCache[aname.Name].Code = null;
                this.assemblyCache[aname.Name].Pdb = null;
            }

            // Devel.ExecutedFrom(string.Format("Loaded {0}", args.LoadedAssembly.ToString()));
        }

        /// <summary>
        /// The on current domain assembly resolve.
        /// </summary>
        /// <param name="sender">
        /// The sender.
        /// </param>
        /// <param name="args">
        /// The args.
        /// </param>
        /// <returns>
        /// </returns>
        private Assembly OnCurrentDomainAssemblyResolve(object sender, ResolveEventArgs args)
        {
            return this.LoadAssembly(args.Name);
        }

        /// <summary>
        /// The on current domain type resolve.
        /// </summary>
        /// <param name="sender">
        /// The sender.
        /// </param>
        /// <param name="args">
        /// The args.
        /// </param>
        /// <returns>
        /// </returns>
        private Assembly OnCurrentDomainTypeResolve(object sender, ResolveEventArgs args)
        {
            // Devel.ExecutedFrom(string.Format("Type not found {0}", args.Name));

            // Explicitely fail. Don't allow any other types than those known in cache
            return null;
        }

        #endregion

        /// <summary>
        /// The assembly resource.
        /// </summary>
        public abstract class AssemblyResource
        {
            #region Properties

            /// <summary>
            /// Gets or sets Assembly.
            /// </summary>
            public Assembly Assembly { get; set; }

            /// <summary>
            /// Gets or sets Code.
            /// </summary>
            public byte[] Code { get; set; }

            /// <summary>
            /// Gets or sets Pdb.
            /// </summary>
            public byte[] Pdb { get; set; }

            /// <summary>
            /// Gets or sets a value indicating whether UseForIntrospection.
            /// </summary>
            public bool UseForIntrospection { get; set; }

            #endregion

            /// <summary>
            /// The database.
            /// </summary>
            public class Database : AssemblyResource
            {
            }

            /// <summary>
            /// The resident.
            /// </summary>
            public class Resident : AssemblyResource
            {
            }
        }
    }
}